package utils

import models.TableData
import slick.ast.TypedType
import slick.jdbc.JdbcProfile
import slick.lifted._

import scala.concurrent.ExecutionContext
import scala.language.higherKinds

trait RichQuery[E, U, C[_]] extends ExtensionMethodConversions {
  val query: Query[E, U, C]

  def filterIf[T <: Rep[_]](f: => E => T)(sure: Boolean)(
    implicit
    wt: CanBeQueryCondition[T]): Query[E, U, C] = {
    if (sure) {
      query.filter(f)
    } else {
      query
    }
  }

  def richLike[T, T2, R](f: => E => Rep[T])(likeStr: String)(
    implicit
    typedType: TypedType[String],
    strExt: Rep[T] => StringColumnExtensionMethods[T],
    om: OptionMapperDSL.arg[String, T]#arg[String, String]#to[Boolean, R],
    wt: CanBeQueryCondition[Rep[R]]): Query[E, U, C] = {
    val strs = likeStr.split(" ").map(_.trim).filterNot(_.isEmpty)
    strs.foldLeft(query) { (newQuery, eachStr) =>
      newQuery.filter(rep => f(rep).like(LiteralColumn(s"%$eachStr%")))(wt)
    }
  }

  def likeIf[T, T2, R](f: => E => Rep[T])(likeStr: String)(sure: Boolean)(
    implicit
    typedType: TypedType[String],
    strExt: Rep[T] => StringColumnExtensionMethods[T],
    om: OptionMapperDSL.arg[String, T]#arg[String, String]#to[Boolean, R],
    wt: CanBeQueryCondition[Rep[R]]): Query[E, U, C] = {
    if (sure) {
      query.filter(rep => f(rep).like(LiteralColumn(likeStr)))(wt)
    } else {
      query
    }
  }

  def actionWithPage(slickParam: SlickParam)(
    implicit
    slickProfile: JdbcProfile,
    ec: ExecutionContext): slickProfile.api.DBIO[(Seq[U], Int)] = {
    CommonResult.commonResult(query.to[Seq], query.to[Seq], slickParam)
  }

  def tableDataWithPage(slickParam: SlickParam)(
    implicit
    slickProfile: JdbcProfile,
    ec: ExecutionContext): slickProfile.api.DBIO[TableData[Seq[U]]] = {
    CommonResult.commonResult(query.to[Seq], query.to[Seq], slickParam).map {
      case (data, size) =>
        TableData(data = data, count = Option(size))
    }
  }

}

case class SlickRange(drop: Int, take: Option[Int])
case class SlickPage(pageIndex: Int, pageSize: Int)
case class ColumnOrder(columnName: String, isDesc: Boolean)

case class SlickParam(
  range: Option[SlickRange] = Option.empty,
  page: Option[SlickPage] = Option.empty) {

  def pageIndex(index: Int): SlickParam = {
    if (index < 0) {
      this.copy(page = Option.empty)
    } else {
      this.copy(
        page = Option(
          SlickPage(
            pageIndex = index,
            pageSize = this.page.map(_.pageSize).getOrElse(10))))
    }
  }

  def pageSize(size: Int): SlickParam = {
    if (size < 0) {
      this.copy(page = Option.empty)
    } else {
      this.copy(
        page = Option(
          SlickPage(
            pageIndex = this.page.map(_.pageIndex).getOrElse(0),
            pageSize = size)))
    }
  }

  def range(range: SlickRange): SlickParam = {
    this.copy(range = Option(range))
  }

  def rangeOpt(rangeOpt: Option[SlickRange]): SlickParam = {
    this.copy(range = rangeOpt)
  }

  def drop(drop: Int): SlickParam = {
    this.copy(range =
      Option(SlickRange(drop = drop, take = this.range.flatMap(_.take))))
  }

  def take(take: Int): SlickParam = {
    this.copy(
      range = Option(
        SlickRange(
          drop = this.range.map(_.drop).getOrElse(0),
          take = Option(take))))
  }

}

object SlickParam {
  def empty: SlickParam =
    new SlickParam(range = Option.empty, page = Option.empty)
}

object CommonResult {

  def commonResult[E, U](
    commonQuery: Query[_, _, Seq],
    sortedQuery: Query[E, U, Seq],
    slickParam: SlickParam)(
    implicit
    slickProfile: JdbcProfile,
    ec: ExecutionContext): slickProfile.api.DBIO[(Seq[U], Int)] = {
    val profile = slickProfile
    import profile.api._
    val mappedQuery = commonQuery

    val result: DBIO[(Seq[U], Int)] = {
      try {

        val baseQuery = sortedQuery

        slickParam match {
          case SlickParam(Some(SlickRange(drop1, Some(take1))),
            Some(SlickPage(pageIndex1, pageSize1))) =>
            val startCount = Math.max(0, drop1)
            val pageIndex = Math.max(0, pageIndex1)
            val pageSize = Math.max(0, pageSize1)

            val dropQuery = mappedQuery.drop(startCount)

            (for {
              sum <- dropQuery.size.result
            } yield {
              val pageStart = startCount + pageIndex * pageSize
              val pageEnd = pageStart + pageSize
              val endCount = Math.min(take1, startCount + sum)
              val autalStart = Math.max(pageStart, startCount)
              val autalEnd = Math.min(pageEnd, endCount)
              val autalLimit = Math.max(0, autalEnd - autalStart)

              val limitQuery = baseQuery
                .drop(startCount)
                .drop(pageIndex * pageSize)
                .take(autalLimit)

              limitQuery.result.map(s => {
                (s, endCount - startCount)
              })
            }).flatMap(s => s)

          case SlickParam(Some(SlickRange(drop, Some(take))), None) =>
            val dropQuery = mappedQuery.drop(drop)

            baseQuery
              .drop(drop)
              .take(take - drop)
              .result
              .map(s => {
                (s, s.size)
              })

          case SlickParam(Some(SlickRange(drop1, None)),
            Some(SlickPage(pageIndex1, pageSize1))) =>
            val startCount = Math.max(0, drop1)
            val pageIndex = Math.max(0, pageIndex1)
            val pageSize = Math.max(0, pageSize1)

            val dropQuery = mappedQuery.drop(startCount)

            (for {
              sum <- dropQuery.size.result
            } yield {

              val limitQuery = baseQuery
                .drop(startCount)
                .drop(pageIndex * pageSize)
                .take(pageSize)

              limitQuery.result.map(s => {
                (s, sum)
              })
            }).flatMap(s => s)

          case SlickParam(Some(SlickRange(drop, None)), None) =>
            baseQuery
              .drop(drop)
              .result
              .map(s => {
                (s, s.size)
              })

          case SlickParam(None, Some(SlickPage(pageIndex, pageSize))) =>
            val dropQuery = baseQuery.drop(pageIndex * pageSize)
            val takeQuery = dropQuery.take(pageSize)

            for {
              sum <- mappedQuery.size.result
              s <- takeQuery.result
            } yield {
              (s, sum)
            }
          case _ =>
            baseQuery.result.map(s => {
              (s, s.size)
            })
        }

      } catch {
        case e: Exception =>
          DBIO.failed(e)
      }

    }
    result

  }

}
